/**
 * @author Fangning CHENG (Alpha.L Cheng) <chengf@purdue.edu>
 *
 * @date Nov 8, 2013
 */
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.TreeMap;

import edu.purdue.cs.cs180.channel.*;

public class Server implements MessageListener {
    Channel                                   channel;
    TreeMap<String, TreeMap<String, Integer>> timeTable        = new TreeMap<String, TreeMap<String, Integer>>();
    Map<String, BldDist[]>                    distanceTable;

    Algorithm                                 algorithm        = Algorithm.NONE;

    Queue<EndPointInfo>                       requesters       = new LinkedList<EndPointInfo>();
    Queue<EndPointInfo>                       normalRequesters = new LinkedList<EndPointInfo>();
    Queue<EndPointInfo>                       urgentRequesters = new LinkedList<EndPointInfo>();
    Queue<EndPointInfo>                       emergRequesters  = new LinkedList<EndPointInfo>();
    Queue<EndPointInfo>                       volunteers       = new LinkedList<EndPointInfo>();

    Map<String, LinkedList<RequesterUnit>>    lbrequesters     = new TreeMap<String, LinkedList<RequesterUnit>>();
    Map<String, LinkedList<Integer>>          lbvolunteers     = new TreeMap<String, LinkedList<Integer>>();

    enum Algorithm {
        NONE, FCFS, CLOSEST, URGENCY
    };

    protected class EndPointInfo {
        public final String building;
        public final String urgency;
        public final int    clientID;

        EndPointInfo(String building, int clientID) {
            this.building = building;
            this.clientID = clientID;
            this.urgency = null;
        }

        EndPointInfo(String building, int clientID, String urgency) {
            this.building = building;
            this.clientID = clientID;
            this.urgency = urgency;
        }
    }

    protected class BldDist {
        public final String buildingName;
        public final int    time;

        public BldDist(String buildingName, int time) {
            this.buildingName = buildingName;
            this.time = time;
        }
    }

    protected class RequesterUnit {
        public final int    requesterID;
        public final String urgency;

        public RequesterUnit(int requesterID, String urgency) {
            this.requesterID = requesterID;
            this.urgency = urgency;
        }
    }

    Server(Channel cnl, String algorithm) {
        channel = cnl;
        initialize();
        if (algorithm.equals("FCFS")) {
            this.algorithm = Algorithm.FCFS;
            System.err.println("\tFCFS Server selected");
        }

        if (algorithm.equals("CLOSEST")) {
            this.algorithm = Algorithm.CLOSEST;
            System.err.println("\tCLOSEST Server selected");
        }

        if (algorithm.equals("URGENCY")) {
            this.algorithm = Algorithm.URGENCY;
            System.err.println("\tURGENCY Server selected");
        }

        if (this.algorithm == Algorithm.NONE)
            return;

        channel.setMessageListener(this);
        System.err.println("\tProject 7 Server started");
    }

    protected void initialize() {
        TreeMap<String, Integer> cl50 = new TreeMap<String, Integer>();
        cl50.put("CL50", 0);
        cl50.put("EE", 8);
        cl50.put("LWSN", 6);
        cl50.put("PMU", 5);
        cl50.put("PUSH", 4);
        timeTable.put("CL50", cl50);

        TreeMap<String, Integer> ee = new TreeMap<String, Integer>();
        ee.put("CL50", 8);
        ee.put("EE", 0);
        ee.put("LWSN", 4);
        ee.put("PMU", 2);
        ee.put("PUSH", 5);
        timeTable.put("EE", ee);

        TreeMap<String, Integer> lwsn = new TreeMap<String, Integer>();
        lwsn.put("CL50", 6);
        lwsn.put("EE", 4);
        lwsn.put("LWSN", 0);
        lwsn.put("PMU", 3);
        lwsn.put("PUSH", 1);
        timeTable.put("LWSN", lwsn);

        TreeMap<String, Integer> pmu = new TreeMap<String, Integer>();
        pmu.put("CL50", 5);
        pmu.put("EE", 2);
        pmu.put("LWSN", 3);
        pmu.put("PMU", 0);
        pmu.put("PUSH", 7);
        timeTable.put("PMU", pmu);

        TreeMap<String, Integer> push = new TreeMap<String, Integer>();
        push.put("CL50", 4);
        push.put("EE", 5);
        push.put("LWSN", 1);
        push.put("PMU", 7);
        push.put("PUSH", 0);
        timeTable.put("PUSH", push);

        distanceTable = new TreeMap<String, BldDist[]>();
        distanceTable.put("CL50", new BldDist[] { new BldDist("CL50", 0),
            new BldDist("PUSH", 4), new BldDist("PMU", 5),
            new BldDist("LWSN", 6), new BldDist("EE", 8) });
        distanceTable.put("EE", new BldDist[] { new BldDist("EE", 0),
            new BldDist("PMU", 2), new BldDist("LWSN", 4),
            new BldDist("PUSH", 5), new BldDist("CL50", 8) });
        distanceTable.put("LWSN", new BldDist[] { new BldDist("LWSN", 0),
            new BldDist("PUSH", 1), new BldDist("PMU", 3),
            new BldDist("EE", 4), new BldDist("CL50", 6) });
        distanceTable.put("PMU", new BldDist[] { new BldDist("PMU", 0),
            new BldDist("EE", 2), new BldDist("LWSN", 3),
            new BldDist("CL50", 5), new BldDist("PUSH", 7) });
        distanceTable.put("PUSH", new BldDist[] { new BldDist("PUSH", 0),
            new BldDist("LWSN", 1), new BldDist("CL50", 4),
            new BldDist("EE", 5), new BldDist("PMU", 7) });
    }

    @Override
    public void messageReceived(String message, int clientID) {
        switch (algorithm) {
            case FCFS:
                fcfsMessageReceived(message, clientID);
                break;
            case URGENCY:
                uMessageReceived(message, clientID);
                break;
            case CLOSEST:
                lbMessageReceived(message, clientID);
                break;
            default:
                break;
        }
    }

    protected boolean sendMessages(int volunteerID, int requesterID,
            int volunteerTime, String requesterBuilding,
            String requesterUrgency, String volunteerBuilding) {
        try {
            channel.sendMessage(String.format("VOLUNTEER %d %d", volunteerID,
                    volunteerTime), requesterID);

            channel.sendMessage(String.format("LOCATION %s %s",
                    requesterBuilding, requesterUrgency), volunteerID);

        } catch (ChannelException e) {
            return false;
        }
        return true;
    }

    protected boolean sendMessages(int volunteerID, int requesterID,
            int volunteerTime, String requesterBuilding, String requesterUrgency) {
        return sendMessages(volunteerID, requesterID, volunteerTime,
                requesterBuilding, requesterUrgency, "Unknown");
    }

    private void lbMessageReceived(String message, int clientID) {
        String msg[] = message.split("\\s+");
        int msgLen = msg.length;

        // Invalid message
        if (msgLen < 2)
            return;
        
        System.err.println(String.format("\t\tMessage from %d: %s", clientID, message));

        int requestType = 0;    // 0: null 1: requester 2: volunteer
        String location = null;
        String urgency = null;
        
        // Request message
        if (msg[0].equals("REQUEST")) {
            if (msgLen < 3) { // Broken message
                System.err.println("\t\t\tBroken REQUEST message");
                return;
            } 
            
            if (!lbrequesters.containsKey(msg[1]))
                // What the hell. Prefer C++ [] syntax
                lbrequesters.put(msg[1], new LinkedList<RequesterUnit>());
            lbrequesters.get(msg[1]).offer(new RequesterUnit(clientID, msg[2]));
            System.err.println(String.format("\t\t\tRequester %d added to queue %s", clientID, msg[1]));
            
            requestType = 1;
            location = msg[1];
            urgency = msg[2];
        }

        // Volunteer message
        if (msg[0].equals("VOLUNTEER")) {
            if (!lbvolunteers.containsKey(msg[1]))
                lbvolunteers.put(msg[1], new LinkedList<Integer>());
            lbvolunteers.get(msg[1]).offer(clientID);
            System.err.println(String.format("\t\t\tVolunteer %d added to queue %s", clientID, msg[1]));
            
            requestType = 2;
            location = msg[1];
        }

        if (requestType == 0) return;   // What the hell message is it?
        

        lbProcessQueue(requestType, location, urgency);

    }

    private void lbProcessQueue(int requestType, String location, String urgency) {
        if (requestType == 1) { // Requester
            for (BldDist buildingDist : distanceTable.get(location)) {
                LinkedList<Integer> volunteerList = lbvolunteers.get(buildingDist.buildingName);

                if (volunteerList == null || volunteerList.isEmpty())    // This building has no available volunteer
                    continue;
                else {  // Pair up
                    int volunteerID = volunteerList.poll();
                    RequesterUnit requester = lbrequesters.get(location).poll();
                    System.err.println(String.format(
                            "\t\tVolunteer %d polled from Queue %s", volunteerID, buildingDist.buildingName));
                    System.err.println(String.format(
                            "\t\tRequester %d polled from Queue %s", requester.requesterID, location));

                    if (!sendMessages(volunteerID, requester.requesterID, buildingDist.time, 
                             location, requester.urgency, buildingDist.buildingName))
                        System.err.println("\t\tWhen processing requester from Requester " + requester.requesterID);
                    break;      // VERY IMPORTANT! DON'T FORGET!
                }
            }
            return;
        }

        if (requestType == 2) { // Volunteer
            for (BldDist buildingDist : distanceTable.get(location)) {
                LinkedList<RequesterUnit> requesterList = lbrequesters.get(buildingDist.buildingName);
                
                if (requesterList == null || requesterList.isEmpty()) // This building does not have a requester
                    continue;
                else {  // Pair up
                    int volunteerID = lbvolunteers.get(location).poll();  // Volunteer just pushed to queue
                    RequesterUnit requester = requesterList.poll();

                    System.err.println(String.format(
                            "\t\tVolunteer %d polled from Queue %s", volunteerID, location));
                    System.err.println(String.format(
                            "\t\tRequester %d polled from Queue %s", requester.requesterID, buildingDist.buildingName));

                    
                    if (!sendMessages(volunteerID, requester.requesterID, buildingDist.time, 
                            buildingDist.buildingName, requester.urgency, location))
                        System.err.println("\t\t\tWhen processing request from Volunteer " + volunteerID);
                    break;      // VERY IMPORTANT! DON'T FORGET IT!
                }
            }
            return;
        }
        
        System.err.println("\tprocessQueue(): Broken requestType");
        return;
    }

    private void uMessageReceived(String message, int clientID) {
        String msg[] = message.split("\\s+");
        int msgLen = msg.length;

        if (msgLen < 2)
            return;

        if (msg[0].equals("REQUEST")) {
            if (msgLen < 3) {
                return;
            }
            if (msg[2].equals("NORMAL"))
                normalRequesters.offer(new EndPointInfo(msg[1], clientID,
                        msg[2]));

            if (msg[2].equals("URGENT"))
                urgentRequesters.offer(new EndPointInfo(msg[1], clientID,
                        msg[2]));

            if (msg[2].equals("EMERGENCY"))
                emergRequesters.offer(new EndPointInfo(msg[1], clientID, msg[2]));

        }

        if (msg[0].equals("VOLUNTEER")) {
            volunteers.offer(new EndPointInfo(msg[1], clientID));
        }

        uProcessQueue();
    }

    private void uProcessQueue() {
        if (volunteers.isEmpty())
            return;

        EndPointInfo requester = emergRequesters.isEmpty() ? urgentRequesters.isEmpty() ? 
                normalRequesters.poll() : urgentRequesters.poll()
                : emergRequesters.poll();

        if (requester == null)
            return;

        EndPointInfo volunteer = volunteers.poll();

        sendMessages(volunteer.clientID, requester.clientID,
                timeTable.get(requester.building).get(volunteer.building),
                requester.building, requester.urgency, volunteer.building);
    }

    private void fcfsMessageReceived(String message, int clientID) {
        String msg[] = message.split("\\s+");
        int msgLen = msg.length;

        if (msgLen < 2)
            return;

        if (msg[0].equals("REQUEST")) {
            if (msgLen < 3) {
                System.err.println("\t\t\tBroken REQUEST message");
                return;
            }
            requesters.offer(new EndPointInfo(msg[1], clientID, msg[2]));
        }

        if (msg[0].equals("VOLUNTEER")) {
            volunteers.offer(new EndPointInfo(msg[1], clientID));
        }

        fcfsProcessQueue();
    }

    private void fcfsProcessQueue() {
        int loopCount = volunteers.size() < requesters.size() ? volunteers.size() : requesters.size();

        for (int i = 0; i < loopCount; ++i) {
            EndPointInfo requester = requesters.poll();
            EndPointInfo volunteer = volunteers.poll();

            sendMessages(volunteer.clientID, requester.clientID,
                    timeTable.get(volunteer.building).get(requester.building),
                    requester.building, requester.urgency, volunteer.building);
        }
    }

    public static void main(String[] args) {
        int port;

        if (args.length > 1) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (Exception ex) {
                System.err.println("Invalid port");
                return;
            }
            if (port < 1024 || port > 65535) {
                System.err.println("Invalid port");
                return;
            }

            try {
                Server srv = new Server(new TCPChannel(port), args[1]);
            } catch (Exception ex) {
                System.err.println(ex.getMessage());
            }

        } else {
            return;
        }
    }
}
